package tk.pak0.audiomidifier.model.midi;

import java.util.ArrayList;
import java.util.List;

import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;


public class MidiTools {
	byte midiChannel = 0; //canal midi por el cual se van a transmitir los mensajes
	boolean finePitch = true; //si fuera false cuantizaría la frecuencia o pitch
    byte pitchBendRage = 2; //Rango +- del pitch bend en semitonos
    boolean velocitySensible = true;
    
    private MidiDevice.Info[] midiDevsInfo;
    private MidiDevice[] midiDevs;
    private List<List<Receiver>> midiReceivers;
    private List<String> tessitura;
    
    private static MidiTools instance=null;
    
	public List<String> getTessitura() {  return tessitura;  }
	public void setTessitura(List<String> tessitura) {  this.tessitura = tessitura;	}
	void setVelocitySensible(boolean b){ velocitySensible = b; }
    boolean getVelocitySensible(){ return velocitySensible; }

    void setMidiChannel(byte midiChan){ midiChannel = midiChan; }
    byte getMidiChannel(){ return midiChannel; }

    void setPitchBendRange(byte newRange){ pitchBendRage = newRange;}
    byte getPitchBendRange(){ return pitchBendRage; }
    
    void setFinePitch(boolean b){ finePitch = b; }
    boolean getFinePitch(){ return finePitch; }
    
    public MidiDevice.Info[] getMidiDevsInfo() {
		return midiDevsInfo;
	}
	public void setMidiDevsInfo(MidiDevice.Info[] midiDevInfo) {
		this.midiDevsInfo = midiDevInfo;
	}
	
	public void setMidiDevs(MidiDevice midiDevs[]) {
		this.midiDevs = midiDevs;
	}
	public MidiDevice[] getMidiDevs() {
		return midiDevs;
	}
	
	public void setMidiReceivers(List<List<Receiver>> midiReceivers) {
		this.midiReceivers = midiReceivers;
	}
	
	public List<List<Receiver>> getMidiReceivers() {
		return midiReceivers;
	}
	
	public static MidiTools getInstance(){
		if(instance == null)
			instance = new MidiTools();
		return instance;
	}
	
    private MidiTools(){
    	midiDevsInfo = initMidiDevsInfo();
    	midiDevs = initMidiDevs();
    	midiReceivers = initMidiReceivers();
    	tessitura = initTessitura();
    }
	
	public MidiDevice.Info[] initMidiDevsInfo(){
		MidiDevice.Info[] midiDevInfo = MidiSystem.getMidiDeviceInfo();
		return midiDevInfo;
	}
	
	public MidiDevice[] initMidiDevs(){
		MidiDevice[] midiDevs = new MidiDevice[getMidiDevsInfo().length];
		int realLength = midiDevs.length;
		for(int i=0 ; i< midiDevs.length ; i++){
			try{
				MidiDevice dev = MidiSystem.getMidiDevice(getMidiDevsInfo()[i]);
				midiDevs[i] = dev;
//				if(dev instanceof Sequencer){
//					midiDevs[i] = dev;
//				}else{
//					midiDevs[i] = null;
//					realLength--;
//				}
//				System.out.println(dev.getDeviceInfo().getName() + " "+dev.getMaxReceivers());
//				try{
//					Receiver rec = dev.getReceiver();
//					System.out.println(dev.getDeviceInfo().getName()+ " "+dev.getClass().toString());
////					System.out.println("Receiver pillado "+rec.toString());
//				}catch (Exception e){
////					System.out.println(dev.getDeviceInfo().getName()+" "+dev.toString());
////					System.out.println("Exception");
//				}
				
//				if(dev instanceof com.sun.media.sound.MidiOutDeviceProvider){
				if(dev.getClass().toString().equals("class com.sun.media.sound.MidiOutDevice")){
					midiDevs[i] = dev;
				}else{
					midiDevs[i] = null;
					realLength--;
				}
			}catch (Exception e){
				e.printStackTrace();
				midiDevs[i] = null;
				realLength--;
			}
		}
		
		MidiDevice[] midiDevsTrim = new MidiDevice[realLength];
		MidiDevice.Info[] midiDevsInfoTrim = new MidiDevice.Info[realLength];
		int j=0;
		for(int i=0; i < midiDevs.length; i++){
			if(j< realLength && midiDevs[i]!= null){
				midiDevsTrim[j] = midiDevs[i];
				midiDevsInfoTrim[j] = midiDevsInfo[i];
				j++;
			}
		}
		
		this.midiDevsInfo = midiDevsInfoTrim;
		
		return midiDevsTrim;
	}
	
	public List<List<Receiver>> initMidiReceivers(){
		List<List<Receiver>> midiReceivers = new ArrayList<List<Receiver>>();
		for(int i=0; i < midiDevs.length; i++){
			if(!midiDevs[i].getReceivers().isEmpty()){
//				MidiDevice dev = midiDevs[i];
				midiReceivers.add(midiDevs[i].getReceivers());
			}
		}
		return midiReceivers;
	}
	
	public List<String> initTessitura(){
		List<String> tessitura = new ArrayList<String>();
		String[] notes = new String[]{"A","B","C","D","E","F","G"};
		for(int i=0;i < 10; i++){
			for(int j=0; j < 14; j++){
				int point = (j % 2)==0 ? j/2: (j-1)/2;
				String sharp = (j % 2)==0 ? "": "#";
				if(!(notes[point].equals("B") || notes[point].equals("E"))){
					tessitura.add(notes[point]+sharp+i);
				}else if(!sharp.equals("#")){
					tessitura.add(notes[point]+sharp+i);
				}
			}
		}
		
		return tessitura;
	}
    
    /**
     * Convierte de frecuencia a Nota, para luego pasarlo a midi
     * @param freq volumen
     * @return Nota
     */
    public Note freqToNota(double freq, byte volumen){
        double rawNote;
        rawNote = (69 + 12 * Math.log10(freq / 440) / Math.log10(2));
        double absNote = Math.abs(rawNote);
        double tail = rawNote - absNote;
        if (tail > 0.5){
            double absNote2=absNote + 1;
            tail = absNote2 - rawNote; // aquí hacemos al tail negativo, es deci, pitch bend down
            absNote = absNote2;
        }
        Note ret = new Note((byte)absNote,tail, volumen);
        return ret;
    }
    /**
     * Este método crea un mensaje Midi que puede ser noteOn o noteOff
     * @param noteOn n
     * @author ffuentes
     * @return midiMes
     */
    public ShortMessage creaMensajeMidi(boolean noteOn, Note n){ //método que crea el mensaje midi de la frecuencia ya cuantizada
        ShortMessage midiMes=new ShortMessage();
        if(noteOn){ //el mensaje es un noteOn
            byte velocity =64;
            if (velocitySensible) //en caso que seleccionemos sensibilidad hace la conversión
                velocity = (byte)(((127 * n.volumen) / 256)); //convertimos de precisión 256 a 128
            try{
                midiMes.setMessage(ShortMessage.NOTE_ON, midiChannel, (byte) n.midiNote, (byte) velocity);
            }catch (Exception e){
                System.err.print(e.toString());
            }
        } else { //el mensaje es un noteOff
            try{
                midiMes.setMessage(ShortMessage.NOTE_OFF, midiChannel,(byte)n.midiNote,0);
            }catch (Exception e){
                System.err.print(e.toString());
            }
        }
        return midiMes;
    }
    /**
     * Dada una nota, devuelve un mensaje midi corto, con el valor del pitch bend
     * @author ffuentes
     * @params n
     * @return msg
     */
    public ShortMessage creaPitchBend(Note n){
        if (!finePitch) //si se ha escogido cuantizar la frecuencia, devuelve null
            return null;
        else {
            ShortMessage msg = new ShortMessage();
            float mult = (float) (n.tail + 1); //convertimos el rango del tail de (-0.5,0.5] a [0.5,1.5]
            int bend = (int) (8192 * mult); //multiplicamos el multiplicador por 8192 para obtener el valor del bend
            byte[] wBend = convierteIntAWord(bend); // lo convertimos a word
            wBend = convierteFormatoBend(wBend); // lo convertimos al formato pitch bend
            try{
                msg.setMessage(ShortMessage.PITCH_BEND, wBend[0], wBend[1]);
            }catch (Exception e){
                System.err.print(e.toString());
                return null; //devolvemos null
            }
            return msg;
        }
    }
    /**
     * Convierte de int a word (un array de dos bytes
     * @author ffuentes
     * @params n
     * @return out
     */
    public byte[] convierteIntAWord(int n) throws NumberFormatException{
        if ( n > 65535) //si es mayor que 0xFFFF
            throw new NumberFormatException("Número int mayor que 65535 / 0xFFFF");
        else{
            String hexStr = Integer.toHexString(n); //pasamos el entero a una cadena con el valor en hexadecimal
            String msbStr = hexStr.substring(12, 14); //cogemos el msb
            String lsbStr = hexStr.substring(14); //cogemos el lsb
            msbStr = new String("0x" + msbStr); //creamos una cadena del tipo 0xXX para msb y lsb
            lsbStr = new String("0x" + lsbStr);
            byte[] out = new byte[2];
            out[1] = Byte.decode(msbStr); // las pasamos a bytesmidiTools
            out[0] = Byte.decode(lsbStr);
            return out; //pafuera
        }
    }
    /**
     * Convierte un int, al formato word manejado por los mensajes Pitchwheel
     * @author ffuentes
     * @params word
     * @return word
     */
    public byte[] convierteFormatoBend(byte[] word){
        if (word[1] > Byte.decode("0x7F")){ // si es mayor que 3F
            byte[] max = new byte[2];
            max[0]=Byte.decode("0xFF");
            max[1]=Byte.decode("0x7F");
            return max; // podríamos lanzar una excepción
        }else{
            int msb = (byte)(word[1]);
            msb = msb << 1; // desplaza el msb a la izquierda
            word[1] = (byte) msb;
            if (word[0] >= 128) // si el bit más significativo de lsb es 1
                word[1] = (byte) (word[1] + 1); //el bit menos significativo de msb se pone a uno
            word[0]= (byte) (word[0] & Byte.decode("0x7F"));
            return word;
        }
    }
}
